feat!: make feign.Request.Body streaming-ready#3360
Conversation
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
| jacksonJaxbJsonProvider.writeTo( | ||
| object, bodyType.getClass(), null, null, APPLICATION_JSON_TYPE, null, outputStream); | ||
| template.body(outputStream.toByteArray(), Charset.defaultCharset()); | ||
| template.body(Request.Body.of(outputStream.toByteArray())); |
There was a problem hiding this comment.
Instead of creating a temporary byte array, could we do something like this?
template.body( Request.Body.of(os -> jacksonJaxbJsonProvider.writeTo(
object, bodyType.getClass(), null, null, APPLICATION_JSON_TYPE, null, os) ) );
This would require a Body.of method that would take an interface that can write body content to an arbitrary stream, so I'm thinking that maybe what I'm suggesting would be some future improvement? If so, then I understand why it is done this way for now.
There was a problem hiding this comment.
While I like the elegance of that suggestion, I find three reasons to keep it as is:
- In vast majority of cases, XML/JSON encoders operate with small chunk of data fitting in-memory.
- It appears that some clients, servers and the
Loggermight benefit fromContent-Lengthheader. And to calculate the length, the data should be eagerly encoded into tangible bytes one way or another. - If there's retry feature enabled,
jacksonJaxbJsonProvider.writeTowill be executed two or more times which is not efficient.
Additionally, I like your idea from the other thread to add support for raw OutputStream and Writer bodies (not implemented in this PR). If there's a really big piece of XML or JSON document, users could leverage raw body supplier to stream the large payload:
interface Client {
@RequestLine("POST /api/v1/send")
@Header("Content-Type: application/json; charset=UTF-8")
void send(ThrowingConsumer<OutputStream, IOException> body);
}
client.send(outputStream -> jacksonJaxbJsonProvider.writeTo(..., outputStream));Alternatively, users can provide custom Encoder implementation with lazy encoding configured.
I'd be happy to hear maintainers' opinion too, and I'm open to any idea.
TL;DR I prefer to keep existing encoders as is and let users enable streaming on demand if needed.
There was a problem hiding this comment.
I agree with you about most json encoders working on relatively small chunks. And I agree that there is really no need to make changes to the existing encoders.
You make a very good point about logging - Feign users rely on body logging, and having the body content in memory makes logging a lot simpler. I actually explored logging with streams in my initial prototypes for this project using buffered streams and mark/reset, but it was overly complicated and did not add sufficient value. Your design is much better.
Note that there is a "streaming" encoder for Feign that looks like it is designed to handle very large json streams, and they might benefit from this (I haven't dug into that encoder in depth), but for the general case, I am in agreement that we should leave the existing encoders alone until there is a compelling reason to change them.
|
I've read over the core changes, and I had one smallish input on this change: "Removed feign.RequestTemplate#charset instance variable — charset should be read from Content-Type headers." I think that this should be carefully considered from a backwards compatibility viewpoint. Some REST servers do not properly set the charset header - in those cases, it is probably important that the Feign user be able to set an override using the @headers annotation. I have not reviewed the code in sufficient detail to know whether this type of override is still possible, but that should probably be checked. We should also be aware that existing Feign users that rely on the old default UTF-8 content type behavior (for example, when interacting with servers that send the wrong content type header in the response), could have problems. I don't really see a good way around this. Personally, I think that this is a breaking change that we should be ok with - the library should have always used the headers for content type determination, and it is better to fix this now, even if it introduced breaking changes for some misconfigured servers. |
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
I believe this is out of #2734 scope. From my experience, there's only a handful of cases where the client might not want to trust the server's If we're talking about the scenario where the server doesn't return |
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
velo
left a comment
There was a problem hiding this comment.
Thanks for the contribution. I can’t approve this in the current state:\n\n1) Required checks are failing (), so this cannot be approved/merged yet.\n2) This changes core public request-body behavior and API surface (, ) with a breaking-change signal. For a compatibility-sensitive library, this needs an explicit compatibility plan and migration path (or clear major-release targeting) before approval.\n\nPlease address CI and provide explicit compatibility/migration details in the PR.
velo
left a comment
There was a problem hiding this comment.
Thanks for the contribution. I can’t approve this in the current state:
- Required checks are failing (ci/circleci: pr-build), so this cannot be approved/merged yet.
- This changes core public request-body behavior and API surface (core/src/main/java/feign/Request.java, core/src/main/java/feign/RequestTemplate.java) with a breaking-change signal (feat!). For a compatibility-sensitive library, this needs an explicit compatibility plan and migration path (or clear major-release targeting) before approval.
Please address CI and provide explicit compatibility/migration details in the PR.
|
I will take a stab at the compatibility plan and migration path (I don't have edit rights on this PR but can include a proposal in comments for @yvasyliev to incorporate). @velo IMPORTANT: The failing circleCI test is because of the breaking changes required for this PR. What is the standard project process for allowing CI to run when there are intentional breaking changes? I've looked all over and can't find a process document that covers this. Thanks. Here's the CI error:
|
|
@yvasyliev here is a first draft of a compatibility/migration plan. Can you please review then add to the PR if you are good with it? CompatibilityThis change includes breaking changes in how Request bodies are handled. For most users, there will be no impact, but any code that interacts directly with request bodies will be impacted. Migrationfeign.Requestbody()Instead of returning a byte[], this method now returns an Optional. If accessing the underlying byte[] is still necessary, call charset()No longer supported. Charset should be an internal implementation detail of the body, not the request. create(...)The following methods have been removed:
Replace these calls with: For simple byte[] bodies, use isBinary()No longer supported. The Body implementation is now responsible for encoding for non-binary content if applicable. length()Use method()This was a deprecated method that has now been removed. Use No longer implements java.io.Serializablefeign.Request is no longer Serializable. feign.RequestTemplatebody(String)use body()use requestBody()Use Note that this alternative method is deprecated and will be removed in later releases. requestCharset()No longer supported. Charset should be an internal implementation detail of the body, not the request or request template. |
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
|
Thank you @trumpetinc ! Hey @velo ,
The ci/circleci: pr-build has passed. Could you please review the PR? Thank you! |
velo
left a comment
There was a problem hiding this comment.
Thanks for the updates and for adding migration notes. I still can’t approve this PR in its current form.
Gate results:
- Test coverage: PASS (extensive test updates are present).
- Backwards compatibility: FAIL.
- Security: PASS.
Blocking issue (compatibility):
- This PR intentionally removes/changes core public API in
core/src/main/java/feign/Request.javaandcore/src/main/java/feign/RequestTemplate.java(e.g.,Request.Bodyshape andRequest.create(...)/body access patterns), and addsMIGRATION-v14.mddescribing the break. - For a compatibility-sensitive library, this cannot be approved on a normal PR track unless it is explicitly handled as a major-release-only change with clear release governance.
Please split this into a major-version release plan (or stage it behind a compatibility-preserving approach) before it can be approved.
Sorry @velo I'm not sure I understand what action items are pending on this PR to have the green light. Do I need to bump the library version to |
|
@kdavisk6 I wanted to pull you into this discussion. The proposal you and I discussed last year has gotten solid traction, and @yvasyliev has a PR that makes the changes necessary for Feign to support streaming data while minimizing impact to regular users. As we discussed last year, this will require a breaking change - but the impact is restricted to library users who are interacting directly with Request. Users that use Feign "normally" using annotations, etc will not be impacted. If you have any feedback on how we can effectively navigate the PR process for a breaking change like this i would appreciate it! Thanks, K |
trumpetinc
left a comment
There was a problem hiding this comment.
I did another full review and have added a few minor items.
I also reviewed the migration guide in detail and provided some feedback. All in all, this is an excellent document (probably one of the best I've seen across many projects I've worked on).
|
|
||
| /** An immutable request to an http server. */ | ||
| public final class Request implements Serializable { | ||
| public final class Request { |
There was a problem hiding this comment.
I guess I should ask why we are dropping Serializable? Personally, I don't think it should have been there in the first place, but removing it adds more to the breaking change footprint. Is there any disadvantage to leaving it?
There was a problem hiding this comment.
I have no clue why it was Serializable without a serialVersionUID. I don't find any benefit of keeping it since feign.Request#body is no longer serializable
I hope the maintainers will let us know if it's necessary to keep it
There was a problem hiding this comment.
I completely agree in principle (that should never have passed code review when it was originally written).
My concern is that this might create more friction to us getting the PR accepted - and it's not relevant to the big picture we are trying to achieve.
On the flip side, if you are making a breaking change, it's best to break everything that needs to be broken at the same time :-) We'll see what they say.
There was a problem hiding this comment.
@trumpetinc I found this thread - #1193
It seems that we need to either redesign FeignException as part of the major release or keep Request serializable (does it have any value without the body?)
There was a problem hiding this comment.
Ugh, Java serialization was such a broken design.
And the fact that Feign uses the response body field in FeignException to store request bodies is super awkward. FeignException should have been an abstract base class with different subclasses for requests vs responses.
But that is not our problem to solve.
Here is my proposal - do you think this is a reasonable compromise?
Make FeignExceptiom serializable again (and include a comment about explaining why with link back to the issue you found).
Change the "request" constructor so it accepts Body. It will then call toString() like the Feign logger does. Then convert the string to byte[] using UTF_8 encoding.
And include comments explaining that we know this is weird, but we are having to do this to work around FeignException not having an abstract base class with a separate class for request exceptions vs response exceptions.
There was a problem hiding this comment.
Decided not to break the backward compatibility for FeignException for now. Made the Request serializable again, but the Request#body & RequestTemplate#body are transient now. I'd suggest revising the exception handling in a separate PR
| * @apiNote It's assumed that the byte array can be converted to a string using {@link | ||
| * StandardCharsets#UTF_8} charset. | ||
| */ | ||
| static Body of(byte[] content) { |
There was a problem hiding this comment.
You may want to consider not including this method right now. It is a little confusing having a pure byte oriented call that implicitly involves charsets.
I couldn't find any callers of this, and given potential for confusion, it is probably safest to just not include it.
There was a problem hiding this comment.
It's in use by some encoders (DefaultEncoder, JacksonEncoder, JacksonJaxbJsonEncoder)
JSON encoders should be fine with implicit UTF-8 charset. Otherwise, I need to update the encoding logic (encode to String) which I'm reluctant to do tbh
BodyImpl needs the charset for toString()/logging purposes only. If a given byte[] is not a human-readable sequence of bytes, then the caller shouldn't even bother logging the body (=providing any charset)
There was a problem hiding this comment.
oh weird - my "Callers" search seems to be failing. Sorry about that. Totally agree with leaving it as-is. It is a point of confusion, but not worth changing right now.
| * @param charset the content charset | ||
| * @return a new {@link Body} instance | ||
| */ | ||
| static Body of(byte[] content, Charset charset) { |
There was a problem hiding this comment.
I have a similar question about the need to expose this as public API. I think the of(xxx) methods are primarily about backwards compatibility for character based request bodies, in which case only of(String content, Charset charset) and of(String content) are needed.
I would move the new Request.BodyImpl(...) code into the of(String, Charset) method and remove both of(byte[]....) calls.
| * @return a string representation of the body content | ||
| * @throws IOException if an I/O error occurs while writing the body content to a string | ||
| */ | ||
| default String writeToString(Charset charset) throws IOException { |
There was a problem hiding this comment.
very small thing, and probably not worth worrying about, but I prefer asString(chaset) and asByteArray().
Usually writeToXXX() implies that we will pass XXX in as a target to the method (which isn't the case here or in the writeToByteArray() method).
There was a problem hiding this comment.
It actually was asString at some point. I decided to move to writeToString to emphasize that this is an IO operation, so that asString is not confused with toString, especially considering that the body might be non-repeatable.
I'm now thinking that writeAsString might be a good alternative
Please let me know what option you prefer and I update accordingly
There was a problem hiding this comment.
Ah - good point - the potential for it being an IO operation (and throwing IOException) does make it not quite as clear...
I'm fine with either naming - this was a minor item and I don't feel that strongly about it...
| * @return a byte array containing the body content | ||
| * @throws IOException if an I/O error occurs while writing the body content to a byte array | ||
| */ | ||
| default byte[] writeToByteArray() throws IOException { |
| } | ||
|
|
||
| public Http2Client(HttpClient client) { | ||
| this(client, ForkJoinPool.commonPool()); |
There was a problem hiding this comment.
This seems like it is more than just backwards compatibility. Can you expand on why these changes are being made? I'm not opposed - I would just like to understand what's going on.
There was a problem hiding this comment.
So JDK HTTP Client takes InputStream as request body content. In order to adapt feign.Request.Body to InputStream, I need to leverage a combination of piped input/output streams.
If I just make
BodyPublisher publisher =
BodyPublishers.ofInputStream(
() -> {
PipedInputStream inputStream = new PipedInputStream();
try (PipedOutputStream outputStream = new PipedOutputStream(inputStream)) {
body.writeTo(outputStream);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return inputStream;
});then:
- The whole request body will be cached in-memory.
return inputStreamwill get blocked until the body is fully written to theoutputStream.
To kill two birds with one stone, we need to execute body.writeTo(outputStream) asynchronously and therefore PropagatingPipedInputStream class & feign.http2client.Http2Client#executor instance variable were introduced.
Http2Client(HttpClient) constructor must be sufficient for most users. ForkJoinPool#commonPool is created & managed by JVM (no user intervention is required). By default, the same pool is used when executing CompletableFuture#runAsync(Runnable) while offering CompletableFuture#runAsync(Runnable, Executor) alternative.
Similarly, in case if anyone wants to supply & manage the executor on their own, they can utilize Http2Client(HttpClient, Executor) constructor.
Thus, I kept the constructor to provide both backward compatibility and convenience
There was a problem hiding this comment.
I just did a dive into HttpClient BodyPublisher, and I understand now. It's unfortunate that they made the design decision they did (it's a similar decision to the original Feign design that we are trying to fix with this PR).
Thanks for the explanation. It does make sense that we'd need to do this to properly support streaming.
…hanges and replacements Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
|
@yvasyliev my code review is complete and all of your changes look good to me. Thank you for your hard work on this - I'm hoping we can get approval shortly. |
velo
left a comment
There was a problem hiding this comment.
Thanks for the updated migration notes. I still cannot approve this PR in its current form.
Gate results:
- Test coverage: PASS.
- Backwards compatibility: FAIL.
- Security: PASS.
Blocking issue:
- This still intentionally changes/removes core public API in core/src/main/java/feign/Request.java and core/src/main/java/feign/RequestTemplate.java, including request body access and factory methods. The migration guide documents the break, but it does not change the compatibility gate result.
Please split this into an explicit major-version release plan, or stage it behind a compatibility-preserving approach, before it can be approved.
|
ok, I will move main to |
Co-authored-by: trumpetinc <6618744+trumpetinc@users.noreply.github.com>
|
@velo I had to make the |
Co-authored-by: trumpetinc 6618744+trumpetinc@users.noreply.github.com
Summary
This PR is the first step toward resolving #2734 — enabling request body streaming in Feign. It builds on the foundational work from @trumpetinc in #2754, which had become too outdated to merge directly.
The core idea is to change
feign.Request.Bodyfrom abyte[]-backed structure to a streaming-ready abstraction. This means request bodies are no longer cached in memory unless explicitly needed — enabling Feign to send large files and streams without buffering.Important
This change does not introduce public streaming API in Feign. It only makes the Feign client streaming-ready. Public streaming API is planned to be implemented in a follow-up PR.
Warning
This is a breaking change and is intended to be released as
v14.rc.1.What changed
Core changes
feign.Request.Bodyis now an interface with:writeTo(OutputStream)— the primary write mechanismcontentLength()— returns-1for unknown/streaming bodiesisRepeatable()—falsefor non-repeatable bodieswriteToByteArray()andwriteToString(Charset)— helper methods for repeatable bodies (use with caution)Body.of(...)— factory methods forString,byte[]inputsfeign.Request.BodyImplis the default implementation for repeatable, non-streaming bodies. It overridestoString()for human-readable logging.feign.RequestTemplatenow exposes a singlebody(Request.Body)setter. The oldbody(byte[], Charset)overload is@Deprecatedfor backward compatibility withspring-cloud-openfeign-core.feign.Request.body()now returnsOptional<Request.Body>instead ofbyte[].feign.Request#length()removed — useRequest.Body#contentLength()instead.feign.RequestTemplate#requestBody()returnsOptional<Request.Body>.feign.RequestTemplate#charsetinstance variable — charset should be read fromContent-Typeheaders.HTTP client updates
All
feign.Clientimplementations updated to stream bodies viaRequest.Body#writeTo:DefaultClientbody.get().writeTo(out)ApacheHttp5ClientFeignBodyEntitywrappingRequest.BodyAsyncApacheHttp5ClientClassicRequestBuilder+ClassicToAsyncRequestProducerfor true streamingOkHttpClientRequestBodydelegating tofeign.Request.BodyHttp2ClientBodyPublishers.ofInputStreamviaPipedInputStream/PipedOutputStreamGoogleHttpClientFeignBodyContentinner classJAXRSClientStreamingOutputVertxHttpClientOutputToReadStreambridge (see below)Encoder updates
All encoder implementations updated to call
template.body(Request.Body.of(...)):GsonEncoder,Jackson3Encoder,JAXBEncoder,JacksonJaxbJsonEncoder,Fastjson2Encoder,MoshiEncoder,SOAPEncoder,DefaultEncoder,JsonEncoder,Fastjson2EncoderVert.x streaming bridge
A new
feign.vertx.OutputToReadStreamclass bridges Java blockingOutputStreamto Vert.xReadStream<Buffer>. It is adapted fromio.cloudonix:vertx-java.io(MIT License) with a local compatibility fix to support both Vert.x 4 and Vert.x 5 (usingonCompleteinstead ofandThento avoid a runtime linkage toio.vertx.core.Completableabsent in Vert.x 4).An issue has been filed upstream: cloudonix/vertx-java.io#8. Once fixed upstream,
OutputToReadStreamcan be removed from this repo.VertxFeign.Buildernow requires.vertx(Vertx)in addition to.webClient(WebClient). ANullPointerExceptionwith a descriptive message is thrown if either is missing.FeignExceptionchangesFeignException.errorReading(...)now passesnullas the body (instead ofrequest.body()), since the request body may be a non-repeatable stream and should not be cached.isEmpty().Logging
Loggerupdated to useRequest.Body#toString()(provided byBodyImpl) for repeatable bodies.Metrics
dropwizard-metrics4/5MeteredEncoder: usesbody.contentLength().micrometerMeteredEncoder: readsContent-Lengthheader for recording.mockmoduleRequestKeyno longer stores or comparesCharset.RequestKey.Builder#charset(Charset)removed.Known limitations / future work
spring-cloud-openfeign-corecompatibility:RequestTemplate#body(byte[], Charset)kept as@Deprecated. Once the Spring team migrates tobody(Request.Body), this can be removed.Content-Typeheaders.UTF-8is used as a fallback inwriteToString. A separate issue/PR may be needed.FeignExceptiondesign: Response bodies are still cached asbyte[]inFeignException. Reconsidering this is deferred to a future PR.-Djapicmp.skip=trueproperty provided.Credits
Special thanks to @trumpetinc whose original #2754 laid the groundwork for this change.
Related
feign.form.MultipartFormContentProcessorstreamable (introduceclass MultipartBody implements feign.RequestBody)